// Copyright 2012 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * Provides a class which takes a DocMap and turns it into HTML documentation.
 */

var fs = require('fs'),
    strings = require('ringo/utils/strings'),
    logger = require('ringo/logging').getLogger('DocWriter'),
    DocInfo = require('rdoc/docinfo').DocInfo,
    TypeLinker = require('rdoc/typelinker').TypeLinker,
    fileTemplate = require('rdoc/templates/file'),
    classTemplate = require('rdoc/templates/class'),
    enumTemplate = require('rdoc/templates/enum'),
    typedefTemplate = require('rdoc/templates/typedef'),
    commonTemplate = require('rdoc/templates/common');



/**
 * A class which takes a DocMap and turns it into HTML documentation.
 *
 * @param {DocMap} docMap The doc map to write out.
 * @param {ConfigParser} config The config object for this run.
 *
 * @constructor
 */
var DocWriter = exports.DocWriter = function(docMap, config) {
  /**
   * @type {DocMap}
   * @private
   */
  this.docMap_ = docMap;

  var types;
  /**
   * Cache this here.  We'll be accessing this object a LOT.
   * @type {!Object.<string, DocMap.Entry>}
   * @private
   */
  this.types_ = types = docMap.types;

  /**
   * Object used to parse type expressions and generate linked HTML strings.
   *
   * @type {TypeLinker}
   * @private
   */
  this.linker_ = new TypeLinker(types);

  /**
   * The config object for this run.
   *
   * @type {ConfigParser}
   * @private
   */
  this.config_ = config;

  /**
   * Keep track of the namespace structure.  This namespaces object represents
   * the global namespace.  Each top-level namespace will be a property on it.
   *
   * @type {!Object.<string, !Object>}
   */
  this.globalNamespace = {};

  var prefixMap;
  /**
   * Maps project filename prefixes to their display names.
   *
   * @type {!Object.<{printName: string, filePrefix: string}>}
   * @private
   */
  this.prefixMap = prefixMap = {};

  var paths = config.paths,
      pathList = Object.keys(paths);
  for (var i = 0, len = pathList.length; i < len; ++i) {
    var fullPath = pathList[i],
        path = paths[fullPath];
        
    prefixMap[path.filePrefix] = path.printName;
  }
};


/**
 * Write the documentation for the given doc map.
 */
DocWriter.prototype.write = function() {
  logger.info('Generating documentation pages.');
  
  var docMap = this.docMap_,
      docFiles = docMap.docFiles;

  for (var i = 0, len = docFiles.length; i < len; ++i) {
    var docFile = docFiles[i],
        docInfo = DocInfo.fromFile(docFile);

    this.writeFileDoc(docFile, docInfo);
    if (i % 100 === 0) {
      logger.info(i + ' files documented.');
    }
  }
  logger.info('Complete.  ' + i + ' files documented.');
};


/**
 * Write out a file that outlines the contents of the given file.
 *
 * @param {string} docFile The filename of the JSON doc info file.
 * @param {DocInfo} docInfo The re-hydrated doc info object.
 */
DocWriter.prototype.writeFileDoc = function(docFile, docInfo) {
  var classDocs = [],
      classes = docInfo.classes,
      classNames = Object.keys(classes),

      interfaceDocs = [],
      interfaces = docInfo.interfaces,
      interfaceNames = Object.keys(interfaces),

      enumDocs = [],
      enums = docInfo.enums,
      enumNames = Object.keys(enums),

      typedefDocs = [],
      typedefs = docInfo.typedefs,
      typedefNames = Object.keys(typedefs);

  this.docInfo = docInfo;
  for (var i = 0, len = classNames.length; i < len; ++i) {
    var name = classNames[i],
        classDesc = classes[name],
        classDoc = this.checkForTags(classDesc.classDoc, name);

    // Queue the class description for the file doc page.
    classDocs[i] = {
      name: this.typeLink(name, true),
      desc: classDoc
    };
    
    this.writeClassDoc(name, classDesc, docInfo.rev, true);
  }

  for (var i = 0, len = interfaceNames.length; i < len; ++i) {
    var name = interfaceNames[i],
        interfaceDesc = interfaces[name],
        interfaceDoc = this.checkForTags(interfaceDesc.classDoc, name);

    interfaceDocs[i] = {
      name: this.typeLink(name, true),
      desc: interfaceDoc
    };
    
    this.writeClassDoc(name, interfaceDesc, docInfo.rev, false);
  }

  for (var i = 0, len = enumNames.length; i < len; ++i) {
    var name = enumNames[i],
        enumDoc = this.checkForTags(enums[name].enumDoc, name);

    enumDocs[i] = {
      name: this.typeLink(name, true),
      desc: enumDoc
    };
    
    this.writeEnumDoc(name, enums[name], docInfo.rev);
  }

  for (var i = 0, len = typedefNames.length; i < len; ++i) {
    var name = typedefNames[i],
        typedefDesc = typedefs[name],
        typedefDoc = this.checkForTags(typedefDesc.typedefDoc, name);

    typedefDocs[i] = {
      name: this.typeLink(name, true),
      desc: typedefDoc
    };

    this.writeTypedefDoc(name, typedefs[name], docInfo.rev);
  }

  var fileoverview = docInfo.fileDescriptor.fileOverview || '';
  fileoverview = this.checkForTags(fileoverview, null);

  var parts = docFile.split('/'),
      file = parts[parts.length - 1];
  
  file = file.substr(0, file.length - 4).replace(/\$/g, '_') + 'html';
  var path = parts.slice(0, -1).join('/') + '/html/',
      docPageFile = path + file;

  var docPageFile = path + file,
      filename = docFile.substr(docFile.lastIndexOf('/') + 1);

  parts = filename.split('-');
  var projectPrefix = parts[0],
      projectPath = parts[1].replace('.json', '.js'),
      head = this.prefixMap[projectPrefix] + ': /' +
             projectPath.replace(/\$/g, '/');

  var templateData = {
        head: head,
        rev: docInfo.rev,
        fileoverview: fileoverview,
        classes: classDocs,
        interfaces: interfaceDocs,
        enums: enumDocs,
        typedefs: typedefDocs
      };

  fs.write(docPageFile, fileTemplate.toFileDoc(templateData));
};


/**
 * Write out the doc page for a class.
 *
 * @param [string} name The name of the class being documented.
 * @param {ClassDescriptor} classDesc The class descriptor for the class to
 *    write out.
 * @param {?string} rev The revision of the file being documented.
 * @param {boolean} isClass True if this is a class, false if it's an interface.
 */
DocWriter.prototype.writeClassDoc = function(name, classDesc, rev, isClass) {
  var docFile = (this.config_.outputPath + 'html/' +
                 name.replace(/\./g, '_') + '.html').toLowerCase();

  var baseClass = classDesc.baseType,
      interfaces = classDesc.interfaces || [],
      extendedInts = classDesc.extendedInterfaces || [],
      extendedIntLinks = [],
      interfaceLinks = [],
      inheritedMembers = this.getInheritedMembers(classDesc);

  for (var i = 0, len = interfaces.length; i < len; ++i) {
    interfaceLinks[i] = this.typeLink(interfaces[i]);
  }

  for (i = 0, len = extendedInts.length; i < len; ++i) {
    extendedIntLinks[i] = this.typeLink(extendedInts[i]);
  }

  var params = classDesc.params || [],
      paramDocs = [];

  for (i = 0, len = params.length; i < len; ++i) {
    var param = params[i];

    param.type = this.processTypeExpr(param.type, true);
    param.desc = this.checkForTags(param.desc);

    paramDocs[i] = param;
  }

  var publicProps = [],
      protectedProps = [],
      privateProps = [],
      props = classDesc.properties || {},
      propNames = Object.keys(props).sort(),
      Vis = DocInfo.Vis;

  for (i = 0, len = propNames.length; i < len; ++i) {
    var prop = props[propNames[i]];
    prop.type = this.processTypeExpr(prop.type);
    prop.desc = this.checkForTags(prop.desc, 'this');

    if (prop.visibility === Vis.PRIVATE) {
      privateProps.push(prop);
    }
    else if (prop.visibility === Vis.PROTECTED) {
      protectedProps.push(prop);
    }
    else {
      publicProps.push(prop);
    }
  }
  
  var publicMethods = [],
      protectedMethods = [],
      privateMethods = [],
      methods = classDesc.methods || {},
      methodNames = Object.keys(methods).sort();

  for (i = 0, len = methodNames.length; i < len; ++i) {
    var methodName = methodNames[i],
        method = methods[methodName];
    method.name = methodName;

    var desc = method.desc || '';
    method.desc = this.checkForTags(desc, 'this');
    if ('deprecationReason' in method) {
      var depReason = method.deprecationReason ||
                      'No reason given for deprecation.';

      method.deprecationReason = this.checkForTags(depReason, 'this');
    }

    if (method.returns) {
      var returns = method.returns;
      method.returns.type = this.processTypeExpr(returns.type);
      method.returns.desc = this.checkForTags(returns.desc, 'this');
    }

    var params = method.params || [];

    for (var j = 0, pLen = params.length; j < pLen; ++j) {
      var param = params[j];

      param.type = this.processTypeExpr(param.type, true);
      param.desc = this.checkForTags(param.desc, 'this');
    }
    if (pLen > 0) {
      method.params = params;
    }

    if (method.visibility === Vis.PRIVATE) {
      privateMethods.push(method);
    }
    else if (method.visibility === Vis.PROTECTED) {
      protectedMethods.push(method);
    }
    else {
      publicMethods.push(method);
    }
  }

  var templateData = {
    title: isClass ? "Class" : "Interface",
    cssKind: isClass ? "class" : "interface",
    
    typeName: name,
    rev: rev,

    baseClass: baseClass ? this.typeLink(baseClass) : null,
    interfaces: interfaceLinks,
    extendedInterfaces: extendedIntLinks,

    classDoc: this.checkForTags(classDesc.classDoc, 'this'),
    constrParams: paramDocs,

    publicProps: publicProps,
    protectedProps: protectedProps,
    privateProps: privateProps,

    publicMethods: publicMethods,
    protectedMethods: protectedMethods,
    privateMethods: privateMethods,

    inheritedMembers: inheritedMembers
  };
  
  if ('deprecationReason' in classDesc) {
    templateData.deprecationReason =
        this.checkForTags(classDesc.deprecationReason);
  }

  fs.write(docFile, classTemplate.toClassDoc(templateData));
};


/**
 * Write out the doc page for an enum.
 *
 * @param {string} name The name of the enum being documented.
 * @param {EnumDescriptor} enumDesc  The descriptor for the enum.
 * @param {string} rev The revision being documented.
 */
DocWriter.prototype.writeEnumDoc = function(name, enumDesc, rev) {
  if (name === 'goog.History.EventType') {

    // Trying to save "goog_history_eventtype.html" and
    // "goog_History_EventType.html" overwrites the same file.  I should come
    // up with a better answer, but for now, just don't print the deprecated
    // goog.History.EventType docs (that enum is deprecated anyway).

    return;
  }

  var docFile = (this.config_.outputPath + 'html/' +
                 name.replace(/\./g, '_') + '.html').toLowerCase();

  var map = enumDesc.map || {},
      entries = Object.keys(map).sort(),
      enumType = enumDesc.type,
      enumVals = [];

  for (var i = 0, len = entries.length; i < len; ++i) {
    var entry = entries[i],
        value = map[entry].value,
        doc = this.checkForTags(map[entry].doc);

    // Quote string values.
    if (enumType === 'string' && value) {
      value = '"' + map[entry].value + '"';
    }
    
    if (! value) {
      value = '<expression>';
    }
    
    enumVals.push({
      name: entry,
      value: value,
      doc: doc
    });
  }
  
  var templateData = {
    typeName: name,
    enumDoc: this.checkForTags(enumDesc.enumDoc),
    rev: rev,
    enumType: this.typeLink(enumType),
    enumVals: enumVals
  };

  if ('deprecationReason' in enumDesc) {
    templateData.deprecationReason = enumDesc.deprecationReason;
  }
  
  fs.write(docFile, enumTemplate.toEnumDoc(templateData));
};


/**
 * Write out the doc page for a typdef.
 *
 * @param {string} name The name of the typdef being documented.
 * @param {TypedefDescriptor} typedefDesc  The descriptor for the typedef.
 * @param {string} rev The revision being documented.
 */
DocWriter.prototype.writeTypedefDoc = function(name, typedefDesc, rev) {
  var docFile = (this.config_.outputPath + 'html/' +
                 name.replace(/\./g, '_') + '.html').toLowerCase();  
  
  var templateData = {
    typeName: name,
    rev: rev,
    typedefDoc: this.checkForTags(typedefDesc.typedefDoc),
    typedefType: this.processTypeExpr(typedefDesc.type)
  };

  if ('deprecationReason' in typedefDesc) {
    templateData.deprecationReason = typedefDesc.deprecationReason;
  }

  fs.write(docFile, typedefTemplate.toTypedefDoc(templateData));
};


/**
 * Keep track of namespaces as they are declared.
 *
 * @param {string} type A dot-separated namespaced type.
 */
DocWriter.prototype.registerType = function(type) {
  var parts = type.split('.'),
      globalNamespace = this.globalNamespace,
      types = this.docMap_.types;

  var ns = globalNamespace,
      name;
  for (var i = 0, len = parts.length - 1; i < len; ++i) {
    var next = parts[i],
        parent = parts.slice(0, i - 1).join('.');
    
    if (! Object.prototype.hasOwnProperty.call(types, parent)) {
      ns[next] = ns[next] || {name: next};
      ns = ns[next];
    }
    else {
      --i;
      break;
    }
  }

  return parts.slice(0, i - 1).join('.');
};


/**
 * Generate linked types for parameter and type annotations.
 *
 * @param {string} type The type expression to be evaluated.
 * @param {boolean=} isParam Whether this is a function parameter.  If true,
 *    then the returned text may include optional parameter notation (=)
 *    instead of an undefined union type.
 *
 * @return {string} A normalized type expression, complete with HTML links to
 *    registered types.
 */
DocWriter.prototype.processTypeExpr = function(type, isParam) {
  return this.linker_.processTypeExpr(type, isParam);
};


/**
 * Checks whether the given string is a registered type and, if so, generate
 * an HTML link to the doc page for that type to be included in doc page.
 * If the string is not a type, it will be returned without modification.
 *
 * @param {string} text The string to check and convert if it's a type.
 * @param {boolean=} opt_anchor If true, always generate an <a name="$type">
 *    tag.  href="..." will only be included if the type is a registered type.
 * @param {?string=} opt_type Text may be something like
 *    "namespace.Class#method".  in this case, compare the type name to this
 *    scoped type.  If they are the same, remove it so only the anchor remains.
 * @return {string} HTML link to the type if registered or anchor is true, or
 *    the plain text if not.
 */
DocWriter.prototype.typeLink = function(text, opt_anchor, opt_type) {
  return this.linker_.typeLink(text, opt_anchor, opt_type);
};


/**
 * Check a block of text for various embedded tags (@link, @code).
 *
 * @param {string} text A block of text that may contain {$link}s.
 * @param {?string=} opt_type For links to type members (eg.,
 *    #getContentElement), this is the path to the doc page for the relevant
 *    type.  In doc strings where no type is in scope (eg., in fileoverview
 *    blocks) null may be passed in to disable unscoped member links.
 *
 * @return {string} The processed text.
 */
DocWriter.prototype.checkForTags = function(text, opt_type) {
  var withNewlines = this.checkNewlines(text),
      withLinks = this.checkForLinks(withNewlines, opt_type);

  return this.checkForCode(withLinks);
};


/**
 * RegExp for matching @link tags.
 *
 * @type {RegExp}
 * @private
 */
DocWriter.linkRX_ = /{\@link (.*?)}/g;


/**
 * RegExp for matching @code tags.
 *
 * @type {RegExp}
 * @private
 */
DocWriter.codeRX_ = /{\@code (.*?)}/g;


/**
 * Check a block of test for @link blocks and convert to a link.
 *
 * @param {string} text A block of text that may contain {$link}s.
 * @param {?string=} opt_type For links to type members (eg.,
 *    #getContentElement), this is the path to the doc page for the relevant
 *    type.  In doc strings where no type is in scope (eg., in fileoverview
 *    blocks) null may be passed in to disable unscoped member links.
 *
 * @return {string} The processed text.
 */
DocWriter.prototype.checkForLinks = function(text, opt_type) {
  if (! text) {
    return '';
  }

  var links = text.match(DocWriter.linkRX_) || [];
  for (var i = 0, len = links.length; i < len; ++i) {
    // Get the part after "($link " and before the trailing ")"
    var linkText = links[i],
        link = linkText.slice(7, -1);
        
    if (opt_type === null && link.charAt(0) === '#') {
      text = text.replace(linkText, link);
    }
    else if (opt_type && link.charAt(0) === '#' && opt_type !== 'this') {
      var typeDocPage = opt_type.replace(/\./g, '_') + '.html',
          href = typeDocPage + link;

      text = text.replace(linkText,
                          commonTemplate.genericLink({
                            href: href,
                            text: link
                          }));
    }
    else if (opt_type && link.charAt(0) === '#' && opt_type === 'this') {
      text = text.replace(linkText,
                                  commonTemplate.genericLink({
                                    href: link,
                                    text: link
                                  }));
    }
    else {
      var typeLink = this.typeLink(link, undefined, opt_type);

      if (typeLink !== link) {
        // We got an <a> back
        text = text.replace(linkText, typeLink);
      }
      else {
        if (strings.startsWith(text, 'http')) {
          text = 
              text.replace(linkText,
                           commonTemplate.genericLink({
                            href: link,
                            text: link
                           }));
        }
        else {
          text = text.replace(linkText, link);
        }
      }
    }
  }

  return text;
};


/**
 * Check a block of text for @code-tagged text.
 *
 * @param {string} text The text to check for.
 * @return {string} The processed text.
 */
DocWriter.prototype.checkForCode = function(text) {
  var codes = text.match(DocWriter.codeRX_) || [];
  for (var i = 0, len = codes.length; i < len; ++i) {
    var codeText = codes[i],
        code = codeText.slice(7, -1);

    text = text.replace(codeText, commonTemplate.codeBlock({code: code}));
  }
  
  return text;
};


/**
 * Convert new-lines into <br /> tags when not within a <pre> block.
 * @param {string} text The text to process.
 * @return {string} The processed text.
 */
DocWriter.prototype.checkNewlines = function(text) {
  var buffer = [],
      inPre = false;

  if (! text) {
    return '';
  }

  for (var i = 0, len = text.length; i < len; ++i) {
    var next = text[i];
    if (next !== '\n' && next !== '<') {
      buffer.push(next)
    }
    else {
      // This is either a new-line or a '<' character.
      if (next === '\n' && !inPre) {
        buffer.push('<br />');
        continue;
      }
      
      else if (next === '<' && text.slice(i, i + 5) === '<pre>') {
        inPre = true;
      }
      else if (next === '<' && text.slice(i, 1 + 6) === '</pre>') {
        inPre = false;
      }
      
      buffer.push(next);
    }
  }
  
  return buffer.join('');
};


/**
 * Walk the class or interface inheritance heirarchy and build up a list of
 * inherited members.
 *
 * @param {ClassDescriptor} classDesc Get all inherited members for this
 *    class/interface.
 * @return {Array.<Object>} An array that can be passed to the class file
 *    template as the "inheritedMembers" parameter.
 */
DocWriter.prototype.getInheritedMembers = function(classDesc) {
  var inherited;
  
  var {PUBLIC, PROTECTED, PRIVATE, INHERITED} = DocInfo.Vis;
  if (classDesc.baseType) {
    // This is a class with a super class.

    var baseType = classDesc.baseType;
    
    if (! this.types_[baseType]) {
      return [];
    }

    var jsFile = this.types_[baseType].filename,
        {docFile} = this.config_.getDocFile(jsFile),
        docInfo = DocInfo.fromFile(docFile),
        superClass = docInfo.classes[baseType];

    // Get the members that the super class inherits from its superclass
    inherited = this.getInheritedMembers(superClass);

    var superMembers = {
      from: this.processTypeExpr(baseType),

      publicProps: [],
      publicMethods: [],
      
      protectedProps: [],
      protectedMethods: [],
      
      privateProps: [],
      privateMethods: []
    };
    inherited.unshift(superMembers);

    var props = superClass.properties || {},
        propNames = Object.keys(props).sort(),
        name, prop;

    for (var i = 0, len = propNames.length; i < len; ++i) {
      var name = propNames[i],
      prop = props[name];

      prop.type = this.processTypeExpr(prop.type);
      prop.desc = this.checkForTags(prop, 'this');

      if (prop.visibility === PRIVATE) {
        superMembers.privateProps.push(prop);
      }
      else if (prop.visibility === PROTECTED) {
        superMembers.protectedProps.push(prop);
      }
      else {
        superMembers.publicProps.push(prop);
      }
    }

    var methods = superClass.methods || {},
        methodNames = Object.keys(methods).sort(),
        method;

    for (i = 0, len = methodNames.length; i < len; ++i) {
      name = methodNames[i];
      method = methods[name];

      method.name = name;
      method.desc = this.checkForTags(method.desc, 'this');
      if ('deprecationReason' in method) {
        method.deprecationReason =
            this.checkForTags(method.deprecationReason, 'this');
      }
  
      if (method.returns) {
        var returns = method.returns;
        returns.type = this.processTypeExpr(returns.type);
        returns.desc = this.checkForTags(returns.desc, 'this');
      }

      var params = method.params || {},
          paramNames = Object.keys(params);
  
      for (var j = 0, pLen = paramNames.length; j < pLen; ++j) {
        var paramName = paramNames[j],
            param = params[paramName];
  
        param.type = this.processTypeExpr(param.type, true);
        param.desc = this.checkForTags(param.desc, 'this');
      }

      if (method.visibility === PRIVATE) {
        superMembers.privateMethods.push(method);
      }
      else if (method.visibility === PROTECTED) {
        superMembers.protectedMethods.push(method);
      }
      else {
        superMembers.publicMethods.push(method);
      }
    }
  }
  else if (classDesc.extendedInterfaces &&
           classDesc.extendedInterfaces.length > 0) {
    // This is an interface that extends other interfaces.

    var extendedInterfaces = classDesc.extendedInterfaces,
        extendedDocs = [];

    for (var i = 0, len = extendedInterfaces.length; i < len; ++i) {
      var superFace = extendedInterfaces[i];

      if (! this.types_[superFace]) {
        continue;
      }

      var jsFile = this.types_[superFace].filename,
          {docFile} = this.config_.getDocFile(jsFile),
          docInfo = DocInfo.fromFile(docFile),
          superInterface = docInfo.interfaces[superFace];

      var superDoc = {},
          name = superInterface.name,
          interfaceDocs = {};      

      interfaceDocs.from = name;

      var methods = superInterface.methods,
          methodNames = Object.keys(methods).sort();
      
      var publicMethods = [],
          protectedMethods = [],
          privateMethods = [];

      for (var j = 0, methodCount = methodNames.length; j < methodCount; ++j) {
        var methodName = methodNames[j],
            method = methods[methodName],
            methodDoc = {};

        // Name of the interface being inherited from.
        methodDoc.name = methodName;
        methodDoc.desc = this.checkForTags(method.desc);
        
        if (method.returns) {
          methodDoc.returns = {
            type: this.typeLink(method.returns.type),
            desc: this.checkForTags(method.desc, 'this')
          };
        }
        
        var params = method.params || {},
            paramNames = Object.keys(params),
            paramsDoc = methodDoc.params = [];

        for (var k = 0, paramCount = paramNames.length; k < paramCount; ++k) {
          var paramName = paramNames[k],
              param = params[paramName];

          paramsDoc[k] = {
            name: paramName,
            desc: this.checkForTags(param.desc, 'this'),
            type: this.typeLink(param.type)
          }
        }

        if (method.visibility === PRIVATE) {
          privateMethods.push(methodDoc);
        }
        else if (method.visibility === PROTECTED) {
          protectedMethods.push(methodDoc);
        }
        else {
          publicMethods.push(methodDoc);
        }
      }

      extendedDocs.push({
        from: name,
        inherited: this.getInheritedMembers(superInterface),

        publicProps: [],
        publicMethods: publicMethods,
        
        protectedProps: [],
        protectedMethods: protectedMethods,

        privateProps: [],
        privateMethods:privateMethods
      });
      
      return extendedDocs;
    }
  }

  return inherited || [];
};
